﻿// Copyright (c) 2012 Mokah Nnamezie & Andrew Cooper
// Neo4Net graph database - http://neo4net.codeplex.com
//
// This file is part of Neo4Net - a .Net port of the Neo4j graph database
//
// Neo4Net is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace Neo4Net.Kernel.Impl.Core
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using NUnit.Framework;
    using Neo4Net.GraphDb;

    [TestFixture]
    public class TestLoopRelationships : AbstractNeo4NetTestCase
    {
        [Test]
        public void CanCreateRelationshipBetweenTwoNodesWithLoopsThenDeleteOneOfTheNodesAndItsRelationships()
        {
            INode source = GraphDb.CreateNode(), target = GraphDb.CreateNode();
            source.CreateRelationshipTo(source, MyRelTypes.Test);
            target.CreateRelationshipTo(target, MyRelTypes.Test);
            source.CreateRelationshipTo(target, MyRelTypes.Test);

            NewTransaction();

            foreach (IRelationship rel in target.GetRelationships())
            {
                rel.Delete();
            }
            target.Delete();
        }

        [Test]
        public void CanDeleteNodeAfterDeletingItsRelationshipsIfThoseRelationshipsIncludeLoops()
        {
            INode node = GraphDb.CreateNode();

            TxCreateLoop(node);
            TxCreateRel(node);
            TxCreateLoop(node);

            node.Delete();
            foreach (IRelationship rel in node.GetRelationships())
            {
                rel.Delete();
            }

            Commit();
        }

        private void TxCreateRel(INode node)
        {
            node.CreateRelationshipTo(GraphDb.CreateNode(), MyRelTypes.Test);
            NewTransaction();
        }

        private void TxCreateLoop(INode node)
        {
            node.CreateRelationshipTo(node, MyRelTypes.Test);
            NewTransaction();
        }

        [Test]
        public void CanAddLoopRelationship()
        {
            INode node = GraphDb.CreateNode();
            node.CreateRelationshipTo(node, MyRelTypes.Test);

            NewTransaction();

            NodeManager.ClearCache();

            foreach (Direction dir in Direction.Values)
            {
                int count = 0;
                foreach (IRelationship rel in node.GetRelationships(dir))
                {
                    count++;
                    Assert.That(rel.StartNode, Is.EqualTo(node), "start node");
                    Assert.That(rel.EndNode, Is.EqualTo(node), "end node");
                    Assert.That(rel.GetOtherNode(node), Is.EqualTo(node), "other node");
                }
                Assert.That(count, Is.EqualTo(1), dir.Name + " relationship count");
            }
        }

        [Test]
        public void CanAddManyLoopRelationships()
        {
            TestAddManyLoopRelationships(2);
            TestAddManyLoopRelationships(3);
            TestAddManyLoopRelationships(5);
        }

        private void TestAddManyLoopRelationships(int count)
        {
            foreach (bool[] loop in Permutations(count))
            {
                INode root = GraphDb.CreateNode();
                IRelationship[] relationships = new IRelationship[count];
                for (int i = 0; i < count; i++)
                {
                    if (loop[i])
                    {
                        relationships[i] = root.CreateRelationshipTo(root, MyRelTypes.Test);
                    }
                    else
                    {
                        relationships[i] = root.CreateRelationshipTo(GraphDb.CreateNode(), MyRelTypes.Test);
                    }
                }
                NewTransaction();
                VerifyRelationships(loop.ToString(), root, loop, relationships);
            }
        }

        [Test]
        public void CanAddLoopRelationshipAndOtherRelationships()
        {
            TestAddLoopRelationshipAndOtherRelationships(2);
            TestAddLoopRelationshipAndOtherRelationships(3);
            TestAddLoopRelationshipAndOtherRelationships(5);
        }

        private void TestAddLoopRelationshipAndOtherRelationships(int size)
        {
            for (int i = 0; i < size; i++)
            {
                INode root = GraphDb.CreateNode();
                IRelationship[] relationships = CreateRelationships(size, i, root);
                VerifyRelationships(String.Format("loop on {0} of {1}", i, size), root, i, relationships);
            }
        }

        [Test]
        public void CanAddAndRemoveLoopRelationshipAndOtherRelationships()
        {
            TestAddAndRemoveLoopRelationshipAndOtherRelationships(2);
            TestAddAndRemoveLoopRelationshipAndOtherRelationships(3);
            TestAddAndRemoveLoopRelationshipAndOtherRelationships(5);
        }

        [Test]
        public void GetSingleRelationshipOnNodeWithOneLoopOnly()
        {
            INode node = GraphDb.CreateNode();
            IRelationship singleRelationship = node.CreateRelationshipTo(node, MyRelTypes.Test);
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Outgoing), Is.EqualTo(singleRelationship));
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Incoming), Is.EqualTo(singleRelationship));
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Both), Is.EqualTo(singleRelationship));
            Commit();
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Outgoing), Is.EqualTo(singleRelationship));
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Incoming), Is.EqualTo(singleRelationship));
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Both), Is.EqualTo(singleRelationship));
        }

        [Test]
        public void CannotDeleteNodeWithLoopStillAttached()
        {
            INode node = GraphDb.CreateNode();
            node.CreateRelationshipTo(node, MyRelTypes.Test);
            NewTransaction();
            node.Delete();

            Assert.That(() => Commit(), Throws.TypeOf<TransactionFailureException>());
        }

        [Test]
        public void GetOtherNodeFunctionsCorrectly()
        {
            INode node = GraphDb.CreateNode();
            IRelationship relationship = node.CreateRelationshipTo(node, MyRelTypes.Test);

            // This loop messes up the readability of the test case, but avoids duplicated
            // assertion code. Same assertions withing the transaction as after it has committed.
            for (int i = 0; i < 2; i++)
            {
                Assert.That(relationship.GetOtherNode(node), Is.EqualTo(node));
                CollectionAssert.AreEqual(new List<INode>() { node, node }, relationship.Nodes.ToList());

                // Should throw exception if another node is passed into loop.getOtherNode
                Assert.That(() => relationship.GetOtherNode(GraphDb.ReferenceNode), Throws.TypeOf<NotFoundException>());

                NewTransaction();
            }
        }

        [Test]
        public void GetNewlyCreatedLoopRelationshipFromCache()
        {
            INode node = GraphDb.CreateNode();
            node.CreateRelationshipTo(GraphDb.CreateNode(), MyRelTypes.Test);
            NewTransaction();
            IRelationship relationship = node.CreateRelationshipTo(node, MyRelTypes.Test);
            NewTransaction();
            Assert.That(node.GetSingleRelationship(MyRelTypes.Test, Direction.Incoming), Is.EqualTo(relationship));
        }

        private void TestAddAndRemoveLoopRelationshipAndOtherRelationships(int size)
        {
            foreach (bool[] delete in Permutations(size))
            {
                for (int i = 0; i < size; i++)
                {
                    INode root = GraphDb.CreateNode();
                    IRelationship[] relationships = CreateRelationships(size, i, root);
                    for (int j = 0; j < size; j++)
                    {
                        if (delete[j])
                        {
                            relationships[j].Delete();
                            relationships[j] = null;
                        }
                        NewTransaction();
                    }
                    VerifyRelationships(String.Format("loop on {0} of {1}, delete {2}", i, size, delete), root, i, relationships);
                }
            }
        }

        private static IEnumerable<bool[]> Permutations(int size)
        {
            int max = 1 << size;
            int pos = 0;

            while (pos < max)
            {
                int cur = pos++;
                bool[] result = new bool[size];
                for (int i = 0; i < size; i++)
                {
                    result[i] = (cur & 1) == 1;
                    cur >>= 1;
                }
                yield return result;
            }
        }

        private IRelationship[] CreateRelationships(int count, int loop, INode root)
        {
            INode[] nodes = new INode[count];
            for (int i = 0; i < count; i++)
            {
                if (loop == i)
                {
                    nodes[i] = root;
                }
                else
                {
                    nodes[i] = GraphDb.CreateNode();
                }
            }

            NewTransaction();

            IRelationship[] relationships = new IRelationship[count];
            for (int i = 0; i < count; i++)
            {
                relationships[i] = root.CreateRelationshipTo(nodes[i], MyRelTypes.Test);
                NewTransaction();
            }
            return relationships;
        }

        private void VerifyRelationships(String message, INode root, int loop, params IRelationship[] relationships)
        {
            bool[] loops = new bool[relationships.Length];
            for (int i = 0; i < relationships.Length; i++)
            {
                loops[i] = (i == loop);
            }
            VerifyRelationships(message, root, loops, relationships);
        }

        private void VerifyRelationships(String message, INode root, bool[] loop, params IRelationship[] relationships)
        {
            NodeManager.ClearCache();

            foreach (Direction dir in Direction.Values)
            {
                ISet<IRelationship> expected = new HashSet<IRelationship>();
                for (int i = 0; i < relationships.Length; i++)
                {
                    if (relationships[i] != null && (dir != Direction.Incoming || loop[i]))
                    {
                        expected.Add(relationships[i]);
                    }
                }

                foreach (IRelationship rel in root.GetRelationships(dir))
                {
                    Assert.That(expected.Remove(rel), Is.True, message + ": unexpected relationship: " + rel);
                }
                Assert.That(expected.Count, Is.EqualTo(0), message + ": expected relationships not seen " + expected);
            }
        }
    }
}
